[深入理解计算机系统]CSAPP 第四章学习笔记

  本人仍处于学习中,若有错误,恳请谅解。这里推荐一个B站宝藏UP主——九曲阑干,看他的视频给了我许多帮助,也希望可以帮助到你。

前言

指令集体系结构是什么

  处理器必须执行一系列指令,每条指令执行某个简单操作,如两个数相加。每一条指令会被编码为由一个或多个字节序列组成的二进制格式,即能被处理器识别的机器码。

  一个处理器支持的指令和指令的字节级编码称为它的指令集体系结构——ISA,也就是指令集架构、指令系统结构。

指令集体系结构的作用

  指令集体系结构(ISA)是计算机软件和硬件交互的接口。

  ISA 在编译器编写者(CPU软件)和处理器设计人员(CPU硬件)之间提供了一个概念抽象层,编译器编写者只需要知道允许那些指令,以及它们是如何编码的(即依据ISA了解CPU选用的指令集,明白可以使用那些指令,同时要遵循哪些规范);而处理器设计者必须建造出执行这些指令的处理器(依据ISA来设计处理器)。

  ISA最重要的内涵就是定义处理器上的软件如何构建。

  注:指令集并不储存于CPU中,事实是CPU本身是指令集体系结构(ISA)的一个实现实例。同时一个ISA可能有多个指令集。

Y86-64

  本章定义了一个简单的指令集,通过x86-64的启发,所以命名为“Y86-64”。Y86-64不像x86-64那样复杂,该指令集的数据类型、指令和寻址方式都要少一些,并且字节级编码也比较简单。但是仍然足够完整。

数字硬件设计

  了解处理器中使用的基本构件块,以及它们如何连接起来和操作的。同时介绍一种描述硬件系统控制部分的简单语言——HCL。通过学习,具有对硬件逻辑设计的背景知识。

顺序设计处理器

  给出一个基于顺序操作、功能正常但是有点不太实用的 Y86-64 处理器。该处理器每个时钟内可以执行一条完整的 Y86-64 指令。所以它的时钟必须足够慢,以允许在一个周期内完成所有动作。

流水线化处理器

  以顺序设计为基础,我们进行升级改造,创建一个流水化线的处理器。

Y86-64指令集体系结构

程序员可见的状态

  什么是程序员的可见状态?

  首先,这里的程序员既可以是用汇编代码写程序的人,也可以是产生机器级代码的编译器。

  其次,可见状态是指每一条指令都会去读取或修改处理器的某些部分。例如内存、寄存器、条件码、程序计数器以及程序状态等。

  在Y86-64指令系统中,我们定义了15个64位的程序寄存器,相较于第三章中熟悉的x86-64的指令系统少了一个 %r15 ,是为了降低指令编码的复杂度。每个程序寄存器存储一个64位的字。寄存器 %rsp 被入栈、出栈、调用和返回指令作为栈指针。其余的没有固定的含义或固定值。

  同时Y86-64的指令系统还简化了条件码寄存器,只有 3 个一位的条件码:ZF、SF、OF,它们保存着最近的算术或逻辑指令所造成影响的有关信息。

  程序计数器(PC)是用来存放当前正在执行指令的地址。

  然后关于内存,从概念上来说,内存实际上就是一个很大的字节数组,保存着程序和数据。Y86-64 程序采用 虚拟地址 来引用内存位置。硬件和操作系统软件联合起来将虚拟地址翻译成实际或 物理地址 ,指明数据实际存在内存中哪个地方。

  最后就是程序状态了,程序状态的最后一个部分是状态码Stat,它是用来表明程序执行的总状态。是正常运行呢,还是出现了异常。

Y86-64指令

  类比x86-64的指令集,Y86-64指令集做了一些相应的简化。

1.数据传送指令

  x86-64中的 movq 指令分成了4个不同的指令:irmovq 、rrmovq 、mrmovq 和 rmmovq,分别显示地指明源和目的的格式。

  指令名字的第一个字母表明源操作数的类型,第二个字母表明目的操作数的类型。

  源操作数可以是立即数(i:immediate)、寄存器(r:register)、内存(m:memory),而目的操作数可以是寄存器(r:register)、内存(m:memory)。

  注:两个内存传送指令(mrmovq、rmmovq)中的内存引用方式是简单的基址和偏移量形式。同时,在地址计算中,不支持第二变址寄存器和任何寄存器的伸缩。

  与x86-64一样,不允许从一个内存地址直接传送到另一个内存地址。另外,也不允许将立即数传送到内存。

2.整数操作指令

  在Y86-64中,我们有4个整数操作指令,它们是addq、subq、andq 和 xorq。它们只对寄存器数据进行操作,而x86-64还允许对内存数据进行这些操作。这些指令会设置 3 个条件码 ZF 、SF 和 OF(零、符号和溢出)。其字节级编码如下:

3.跳转指令

  在Y86-64中,我们有7个跳转指令,分别是 jmp 、jle 、jl 、je、jne、jge 和 jg。根据分支指令的类型和条件代码的设置来选择分支。分支条件和 x86-64 一样。如下图所示:

4.条件传送指令

  在 Y86-64 中,我们定义了6个条件传送指令:comvle、cmovl、cmove、cmovne、cmovge、cmovg。这些指令的格式与寄存器-寄存器传送指令 rrmovq 一样,但是只有当条件代码满足所需要的约束时,才会更新目的寄存器的值。

  其字节级编码如下:

5.其他指令

  call指令会将返回地址入栈,然后跳到目的地址。ret 指令从这样的调用中返回。

  pushq 和 popq 指令实现了入栈和出栈,就像在 x86-64 中一样。

  halt 指令停止指令的执行。x86-64 中有一个与之相当的指令 hlt。x86-64 的应用程序不允许使用这条指令,因为它会导致整个系统暂停运行。对于 Y86-64 来说,执行halt 指令会导致处理器停止,并将状态码设置为 HLT 。

  其字节级编码如下:

指令编码

  上图给出了上述指令的字节级编码。每条指令需要1~10个字节不等,这取决于需要哪些字段。

  1. 每条指令的第一个字节表明指令的类型。

  2. 该字节分为两部分,每一部分占4个比特位,高四位表示指令代码,低四位表示指令功能。

  3. 不同的指令代码表示不同的指令,指令的功能部分都为 0 。

  4. 当指令中有寄存器类型的操作数时,回附加一个字节,该字节被称为寄存器指示符字节,用于指定一个或者两个寄存器,因此还需要对寄存器进行编码。

  5. Y86-64指令系统中,我们定义了15个寄存器,每个寄存器不仅定义了名字,还为每一个寄存器指定一个编号。使用十六进制数 0 ~ 0xE 来表示。

  指令集的一个重要性质就是字节编码必须有唯一的解释。任意一个字节序列要么是一个唯一的指令序列的编码,要么就不是一个合法的字节序列。

Y86-64 异常

  对 Y86-64 来说,程序员可见状态包括状态码 Stat ,它描述程序执行的总体状态。这个代码可能的值如下图所示:

出现某种异常时,Stat 的值会变为相对应的值,同时也会被命名为相对应的名字。在遇到这些异常的时候,我们就只是简单的让处理器停止执行指令。

Y86-64 程序

  x86-64代码是由GCC编译器产生的。Y86-64代码与之类似,但又以下不同点:

  • Y86-64将常数加载到寄存器(第2~6行),因为它在算术指令中不能使用立即数。
  • 要实现从内存读取一个数值并将其与一个寄存器相加,Y86-64代码需要两条指令(第8~9行),而x86-64只需要一条 addq 指令(第5行)。
  • 手工编写的Y86-64实现有一个优势,即 subq 指令(第11行)同时还设置了条件码,因此GCC生成代码中的 testq 指令(第9行)就不是必需的。不过为此,Y86-64必须使用 andq 指令(第5行)在进入循环之前设置条件码。

  程序寄存器:从本质上来讲它属于处理器内部的存储单元,通常我们将寄存器的集合称为寄存器文件,有的资料中也称寄存器堆。

  在处理器内部,寄存器文件和算术逻辑单元(ALU)是串联的,寄存器文文件的输出端口与 ALU 的输入端口相连。

  下图展示了一个寄存器文件的功能表述:

  • 具有一个读端口和一个写端口,数据位宽为64位。
  • 规定读写操作共用地址线,由于定义了15个程序寄存器,故地址线宽度设计成4位即可满足寻址要求。
  • 此外,还有时钟信号、复位信号以及写使能信号。